package core;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.interfaces.DSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.List;

// Necesario implementar Serializable para guardarlo en binario (para
// serializarlo, tiene su lógica xD).
public class Publisher implements Serializable {
	// Constantes.
	public static final String ALGORITHM = "DSA";
	private static final String RANDGEN_ALG = "SHA1PRNG";
	private static final int KEYSIZE = 1024;
	public static final String GENERIC_PUBLICKEY_FNAME = "public.key";
	public static final String GENERIC_PRIVATEKEY_FNAME = "private.key";
	public static final int PUBLIC = 1;
	public static final int PRIVATE = 2;
	public static final String PUBLISHEREXT = ".pub";
	
	// Esta constante se declaro para que el compilador no llorara.
	private static final long serialVersionUID = 1;
	
	// Atributos de la clase.
	//@XmlElement
	private String name = null;
	//@XmlElement
	private DSAPublicKey pub_key = null;
	private PrivateKey priv_key = null;
	
	//@XmlElement
	private List<Blog> blogs;
	
	// Funciones privadas
	/**
	 * Establece la clave publica del publisher 
	 * 
	 * */
	private void setPublicKey(DSAPublicKey pubkey){
		this.pub_key = pubkey;
	}
	
	/**
	 * Set the private key of the publisher 
	 * 
	 * */
	private void setPrivateKey(PrivateKey privkey){
		this.priv_key = privkey;
	}
	
	/**
	 * Function used to generate name of the pair of keys for a given publisher.
	 * Using that function is easier than writing that in code various times
	 * The name is created from path, publisher name and a suffix for public or
	 * private key. Ex:
	 * - "<path>/<publisher_name>-public.key": For a public key.
	 * - "<path>/<publisher_name>-private.key": For a private key. 
	 * 
	 * @param path
	 * @param publisher
	 * @param pub_or_priv
	 * @return
	 * @throws Exception 
	 */
	private String buildKeyPath(String path, String publisher,
								int pub_or_priv) throws Exception {
		String res = Util.checkDirectoryPath(path);
		res += publisher;
		res += '-';
		if (pub_or_priv == PUBLIC)
			res += GENERIC_PUBLICKEY_FNAME;
		else if (pub_or_priv == PRIVATE)
			res += GENERIC_PRIVATEKEY_FNAME;
		else
			throw new IllegalArgumentException("");
		
		return res;
	}
	
	// Métodos de clase.
	
	/**
	 * Empty constructor to serialize
	 */
	public Publisher() {};

	/**
	 * Class constructor that make a Publisher object which name is the given
	 * one. Also there are made the pair of private/public key in the path
	 * <path>/<name>-public.key and <path>/<name>-private.key
	 * If the files already exist an exception is thrown.
	 * 
	 * @param name:
	 * 		Publisher's name.
	 * @param path:
	 * 		Path where keys will be saved.
	 * @throws Exception 
	 */
	public Publisher(String name, String path) throws Exception {
		this(name);
		
		KeyPairGenerator kpg;
		KeyPair kp;
		SecureRandom randgen;
		String fname_pub, fname_priv;
		File fd_pub, fd_priv; 
		FileOutputStream fos_pub, fos_priv;
		byte[] pubkeyenc, privkeyenc;				
		
		// Preparamos los ficheros a escribir, comprobando que no existan.
		fname_pub = buildKeyPath(path, name, PUBLIC);
		fname_priv = buildKeyPath(path, name, PRIVATE);
		fd_pub = new File(fname_pub);
		fd_priv = new File(fname_priv);
		if (fd_pub.exists() || fd_priv.exists())
			throw new Exception("There were some of the key files");
		fos_pub = new FileOutputStream(fname_pub);
		fos_priv = new FileOutputStream(fname_priv);
		System.out.println("BEWARE!!!: Ensure (and change if so) that the " +
						   "private key file has read permissions just for " +
						   "the owner. Java doesn't let change it from the " +
						   "code. Sorry.");
		
		// Creando claves.
		kpg = KeyPairGenerator.getInstance(ALGORITHM);
		
		// MIS COJONES DE ALEATORIEDAD --> http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2008-5659
		// Estos aprendieron de la peña de OpenSSL de Debian xD
		randgen = SecureRandom.getInstance(RANDGEN_ALG);
		kpg.initialize(KEYSIZE, randgen);
		kp = kpg.generateKeyPair();
		priv_key = kp.getPrivate();
		pub_key = (DSAPublicKey)kp.getPublic();
		
		// Guardando claves a disco.
		pubkeyenc = pub_key.getEncoded();
		privkeyenc = priv_key.getEncoded();
		fos_pub.write(pubkeyenc);
		fos_priv.write(privkeyenc);
		fos_pub.close();
		fos_priv.close();
	}
	
	// Crea un publisher sin claves asociadas.
	/**
	 * Create a publisher
	 * @param name Publisher name
	 */
	public Publisher(String name) {
		this.name = name;			
		this.blogs = new ArrayList<Blog>();
	}
	
	
	/**
	 * @return Publisher name
	 */
	public String getName() {
		return name;
	}
	
	/**
	 * @return Private key of the publisher 
	 * (If it isn't initialized return null)
	 */
	public PrivateKey getPrivateKey() {
		return priv_key;
	}
	
	/**
	 * @return Public key of the publisher 
	 * (If it isn't initialized return null)
	 */
	public PublicKey getPublicKey() {
		return pub_key;
	}
	
	/**
	 * Load a public key in the publisher
	 * 
	 * @param fname: The filename of the public key.
	 * @throws IOException 
	 * @throws NoSuchProviderException 
	 * @throws NoSuchAlgorithmException 
	 * @throws InvalidKeySpecException 
	 */
	// Cargara una nueva public key (tanto si hubiera una como si no la habia)
	public void importPublicKey(String fname) throws IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
		// Leemos la clave publica a nivel de bytes
		File fd;
		DSAPublicKey pubKeyReaded;
		fd = new File(fname);
//		fd = new File(path 
//				+ System.getProperty("file.separator") + name + "-" + GENERIC_PUBLICKEY_FNAME);
		FileInputStream keyfis = new FileInputStream(fd);
        byte[] encKey = new byte[(int)fd.length()];  
        keyfis.read(encKey);
        keyfis.close();
        
        // Especificamos cómo está codificada la key 
        X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encKey);
        KeyFactory keyFactory = KeyFactory.getInstance("DSA", "SUN");
        
        // Convertimos a objeto de clave publica
        pubKeyReaded = (DSAPublicKey)keyFactory.generatePublic(pubKeySpec);
        this.setPublicKey(pubKeyReaded);
	}
	
	/**
	 * Load a private key in the publisher
	 * 
	 * @param fname: The filename of the private key.
	 * @param password
	 * 			Password utilizado para cifrarla
	 * @throws IOException 
	 * @throws NoSuchProviderException 
	 * @throws NoSuchAlgorithmException 
	 * @throws InvalidKeySpecException 
	 */
	// Cargara una nueva private key, probablemente deberia estar cifrada.
	public void importPrivate(String fname, String password) throws IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
		File fd;
		PrivateKey privKeyReaded;
		if(password.isEmpty()) {
			// La clave privada va sin cifrar
			// Leemos la clave publica a nivel de bytes
			fd = new File(fname);
//			fd = new File(path+ 
//					System.getProperty("file.separator") + name + "-" +GENERIC_PRIVATEKEY_FNAME);
			FileInputStream keyfis = new FileInputStream(fd);
	        byte[] encKey = new byte[(int)fd.length()];  
	        keyfis.read(encKey);
	        keyfis.close();
	        
	        // Especificamos cómo está codificada la key 
	        PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(encKey);
	        KeyFactory keyFactory = KeyFactory.getInstance("DSA", "SUN");
	        // Convertimos a objeto de clave privada
	        privKeyReaded = keyFactory.generatePrivate(privKeySpec);
	        this.setPrivateKey(privKeyReaded);
		}
		else {
			// La clave privada va cifrada
		}
	}
	
	/**
	 * Save publisher to a binary file
	 * 
	 * @param path
	 * 			Path where file will be saved 
	 * @throws Exception 
	 * @exception IllegalArgumentException
	 * 			Invalid path
	 * @exception Exception
	 * 			Error writing data
	 */
	public void save(String path) throws Exception {
		// Se extrae el nombre del fichero automaticamente del nombre del publisher
		path = Util.checkDirectoryPath(path)+ this.getName()+ PUBLISHEREXT;
		// Creamos el fichero y su ObjectOutputStream para poder escribir
		FileOutputStream fichero = new FileOutputStream(path);
		ObjectOutputStream oos = new ObjectOutputStream(fichero);
		// Y escribimos a nivel de bits
		oos.writeObject(this);
	}
	
	/**
	 * Create a publisher from file.
	 * 
	 * @param fname
	 * 			Path to file from where create a publisher
	 * @throws IOException 
	 * @throws ClassNotFoundException 
	 * @exception IllegalArgumentException
	 * 			File not found or invalid
	 * @exception Exception
	 *			Error creating publisher from file
	 */
	public static Publisher createPublisher(String fname) throws IOException, ClassNotFoundException {
		File in = new File(fname);
		Publisher publisher_readed;
		// Comprobamos que el fichero efectivamente no existia
		if (!in.exists()) {
			throw new IllegalArgumentException("File doesn't exist");
		}
		// Y que tengamos permisos para leerlo
		if (!in.canRead()) {
			throw new IllegalArgumentException(
					"Insufficient privileges to read file");
		}
		// Creamos el fichero y su ObjectOutputStream para poder escribir
		FileInputStream fichero = new FileInputStream(fname);
		ObjectInputStream oos = new ObjectInputStream(fichero);
		// Leemos el publisher
		publisher_readed = (Publisher) oos.readObject();
		oos.close();
		return publisher_readed;
	}
	
	/**
	 * Erase publisher made up by Publisher.save method
	 * @param path
	 *            Path where publisher is located
	 * @throws Exception 
	 */
	public boolean remove(String path) throws Exception {
		boolean res = false;
		String checkedpath = Util.checkDirectoryPath(path);
		String fname = checkedpath + this.getName()+ PUBLISHEREXT;
		File file = new File(fname);
		if (file.exists()) 
			res = file.delete();
		return res;
	}
	
	/**
	 * Export key
	 * @param fname Path where key will be saved
	 * @param password Password used to encrypt it
	 * @param publisher Publisher
	 * @throws IOException 
	 */
	private void exportKey(String fname, byte[] data, String password) 
	throws IOException {
		FileOutputStream fos;
		fos = new FileOutputStream(fname);
		fos.write(data);
		fos.close();
		//TODO cuando se use password meter el tema de AES
	}
	
	/**
	 * Export private key
	 * @param fname Path where private key will be saved
	 * @param password Password used to encrypt it
	 * @param publisher Publisher
	 * @throws IOException 
	 */
	public void exportPrivateKey(String fname, String password) 
	throws IOException {		
		byte[] data;
		data = this.getPrivateKey().getEncoded();
		this.exportKey(fname, data, password);
	}
	
	/**
	 * Export public key
	 * @param fname Path where public key will be saved
	 * @param password Password used to encrypt it
	 * @param publisher Publisher
	 * @throws IOException 
	 */
	public void exportPublicKey(String fname, String password) 
	throws IOException {
		byte[] data;
		data = this.getPublicKey().getEncoded();
		this.exportKey(fname, data, password);
	}	
	
	public void AddBlog(Blog blog) {
		this.blogs.add(blog);
	}
	
	public List<Blog> getBlogs()
	{
		return this.blogs;
	}
}
